package org.luxor.commons.playwright.client.impl;

import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Page;
import org.apache.commons.io.IOUtils;
import org.luxor.commons.playwright.client.PlaywrightClient;
import org.luxor.commons.playwright.component.Context;
import org.luxor.commons.playwright.component.ExecState;
import org.luxor.commons.playwright.component.PlaywrightBrowserCreator;
import org.luxor.commons.playwright.component.command.PlaywrightCommand;
import org.luxor.commons.playwright.config.properties.PlaywrightProperties;
import org.luxor.commons.playwright.constant.LaunchMode;
import org.luxor.commons.playwright.exception.StealthJsException;
import org.luxor.commons.playwright.listener.PlaywrightEventListener;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * PlaywrightClient 实现类
 *
 * @author Mr.yan  @date 2022/6/10
 */
public class PlaywrightClientImpl implements PlaywrightClient {

    private final ThreadPoolTaskExecutor asyncExecutor;

    private final PlaywrightProperties properties;

    private final Collection<PlaywrightEventListener> listeners;

    private final PlaywrightBrowserCreator playwrightDriverCreator;

    public PlaywrightClientImpl(PlaywrightBrowserCreator playwrightDriverCreator, PlaywrightProperties properties, Collection<PlaywrightEventListener> listeners) {
        this.playwrightDriverCreator = playwrightDriverCreator;
        this.properties = properties;
        this.listeners = listeners;
        this.asyncExecutor = initThreadPool(properties);
    }

    private ThreadPoolTaskExecutor initThreadPool(PlaywrightProperties properties) {
        ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        threadPool.setCorePoolSize(properties.getEndpoints().size());
        // 设置最大线程数
        threadPool.setMaxPoolSize(properties.getEndpoints().size());
        // 设置队列容量
        threadPool.setQueueCapacity(20);
        // 设置线程空闲时间（秒）
        threadPool.setKeepAliveSeconds(30);
        // 设置默认线程名称
        threadPool.setThreadNamePrefix("playwright-async-");
        // 设置拒绝策略
        threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 等待所有任务结束后再关闭线程池
        threadPool.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        threadPool.initialize();
        return threadPool;
    }

    private void dispatcherInitEvent(PlaywrightCommand command, Context context) {
        for (PlaywrightEventListener listener : this.listeners) {
            listener.initExecute(command, context);
        }
    }

    private void dispatcherBeforeEvent(Page page, PlaywrightCommand command, Context context) {
        for (PlaywrightEventListener listener : this.listeners) {
            try {
                listener.beforeExecute(page, command, context);
            } catch (Throwable throwable) {
                listener.onException(throwable, page);
            }
        }
    }

    private <T> T run(Page page, PlaywrightCommand<T> command, Context context) {
        try {
            return command.execute(page, context);
        } catch (Throwable e) {
            if (ExecState.RUNNING.equals(context.getState()) && context.getExecuteCount() <= getProperties().getRetryNumMax()) {
                // 执行次数+1
                context.incrementAndExecuteCount();
                // 执行重试
                return run(page, command, context);
            }
            throw e;
        }
    }

    private void dispatcherAfterEvent(Page page, PlaywrightCommand command, Context context) {
        for (PlaywrightEventListener listener : this.listeners) {
            try {
                listener.afterExecute(page, command, context);
            } catch (Throwable throwable) {
                listener.onException(throwable, page);
            }
        }
    }

    /**
     * 执行浏览器驱动指令
     *
     * @param command 驱动指令
     * @return T
     */
    @Override
    public <T> T execute(PlaywrightCommand<T> command) {
        Context context = new Context();
        Page page = null;
        try {
            dispatcherInitEvent(command, context);
            context.execInitialize(getProperties(), command);

            // 获取浏览器运行实例
            Browser browser = playwrightDriverCreator.createBrowser();

            // 模拟真实浏览器请求头
            Map<String, String> extraHTTPHeaders = new HashMap<>();
            extraHTTPHeaders.put("sec-ch-ua", "\"Chromium\";v=\"112\", \"Google Chrome\";v=\"112\", \"Not:A-Brand\";v=\"99\"");
            extraHTTPHeaders.put("sec-ch-ua-mobile", "?0");
            extraHTTPHeaders.put("sec-ch-ua-platform", "\"macOS\"");
            String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36";
            Browser.NewContextOptions options = new Browser.NewContextOptions().setExtraHTTPHeaders(extraHTTPHeaders).setUserAgent(userAgent);

            // 创建新的浏览器上下文
            BrowserContext browserContext = browser.newContext(options);
            // 抹除webDrive特征，绕开防爬虫监测
            if (properties.getCamouflageEnabled()) {
                if (LaunchMode.LAUNCH.equals(properties.getLaunchMode())) {
                    try {
                        // https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions
                        // https://raw.githubusercontent.com/requireCool/stealth.min.js/main/stealth.min.js
                        ClassPathResource stealthJsResource = new ClassPathResource("org/luxor/commons/playwright/js/stealth.min.js");
                        browserContext.addInitScript(IOUtils.toString(stealthJsResource.getInputStream(), StandardCharsets.UTF_8));
                    } catch (Exception e) {
                        throw new StealthJsException("爬虫特征码抹除脚本(stealth.min.js),执行失败", e);
                    }
                }
                try {
                    ClassPathResource stealthJsResource = new ClassPathResource("org/luxor/commons/playwright/js/stealth.extend.js");
                    browserContext.addInitScript(IOUtils.toString(stealthJsResource.getInputStream(), StandardCharsets.UTF_8));
                } catch (Exception e) {
                    throw new StealthJsException("爬虫特征码抹除脚本(stealth.extend.js),执行失败", e);
                }
            }
            // 创建新的操作页
            page = browserContext.newPage();
            context.execStart();
            dispatcherBeforeEvent(page, command, context);
            T result = run(page, command, context);
            context.execCompleted();
            return result;
        } catch (Throwable e) {
            context.execFailed(page, e);
            throw e;
        } finally {
            dispatcherAfterEvent(page, command, context);
            playwrightDriverCreator.closeBrowser(page.context().browser());
        }
    }

    /**
     * 异步执行浏览器驱动指令
     *
     * @param command 驱动指令
     * @return T
     */
    @Override
    public <T> Future<T> executeAsync(PlaywrightCommand<T> command) {
        return asyncExecutor.submit(new PlaywrightClientImpl.AsyncExecuteCallable<>(command));
    }

    class AsyncExecuteCallable<T> implements Callable<T> {
        private final PlaywrightCommand<T> command;

        public AsyncExecuteCallable(PlaywrightCommand<T> command) {
            this.command = command;
        }

        @Override
        public T call() {
            Context context = new Context();
            Page page = null;
            try {
                dispatcherInitEvent(command, context);
                context.execInitialize(getProperties(), command);
                // 获取浏览器运行实例
                Browser browser = playwrightDriverCreator.createBrowser();
                // 创建新的浏览器上下文
                BrowserContext browserContext = browser.newContext(new Browser.NewContextOptions()
                        .setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
                );
                // 抹除webDrive特征，绕开防爬虫监测
                if (properties.getCamouflageEnabled()) {
                    if (LaunchMode.LAUNCH.equals(properties.getLaunchMode())) {
                        try {
                            // https://github.com/berstend/puppeteer-extra/tree/master/packages/extract-stealth-evasions
                            // https://raw.githubusercontent.com/requireCool/stealth.min.js/main/stealth.min.js
                            ClassPathResource stealthJsResource = new ClassPathResource("org/luxor/commons/playwright/js/stealth.min.js");
                            browserContext.addInitScript(IOUtils.toString(stealthJsResource.getInputStream(), StandardCharsets.UTF_8));
                        } catch (Exception e) {
                            throw new StealthJsException("爬虫特征码抹除脚本(stealth.min.js),执行失败", e);
                        }
                    }
                    try {
                        ClassPathResource stealthJsResource = new ClassPathResource("org/luxor/commons/playwright/js/stealth.extend.js");
                        browserContext.addInitScript(IOUtils.toString(stealthJsResource.getInputStream(), StandardCharsets.UTF_8));
                    } catch (Exception e) {
                        throw new StealthJsException("爬虫特征码抹除脚本(stealth.extend.js),执行失败", e);
                    }
                }
                // 创建新的操作页
                page = browserContext.newPage();
                context.execStart();
                dispatcherBeforeEvent(page, this.command, context);
                T result = run(page, this.command, context);
                context.execCompleted();
                return result;
            } catch (Throwable e) {
                context.execFailed(page, e);
                throw e;
            } finally {
                dispatcherAfterEvent(page, this.command, context);
                if (page != null) {
                    playwrightDriverCreator.closeBrowser(page.context().browser());
                }
            }
        }
    }

    public PlaywrightProperties getProperties() {
        return this.properties;
    }


}
